ANDES: fix run_eigenvalue_analysis + add PSS/E raw+dyr dynamic-model loading - #52
Open
elasticdotventures wants to merge 2 commits into
Open
ANDES: fix run_eigenvalue_analysis + add PSS/E raw+dyr dynamic-model loading#52elasticdotventures wants to merge 2 commits into
elasticdotventures wants to merge 2 commits into
Conversation
ss.EIG.vectors and ss.EIG.state_desc do not exist on ANDES's real EIG
routine object; the hasattr() guards around them meant eigenvectors
and state_variables have silently returned empty lists since this
tool was written. Verified against andes/routines/eig.py's source and
a live andes 2.0.0 install run against the bundled Kundur case
(ANDES/kundur_full.json):
ss.EIG.mu eigenvalues (complex array)
ss.EIG.N, .W right/left eigenvector matrices
ss.EIG.pfactors participation factors
ss.EIG.x_name state labels
run_eigenvalue_analysis now reads these directly and computes
frequency_hz/damping_ratio_pct per mode using the same formula
EIG.post_process() uses internally for its own text report:
freq_hz = |Im(mu)| / (2*pi)
damping_pct = -100 * Re(mu) / |mu|
Modes are returned sorted least-damped (most concerning) first.
Also:
- Factor tests/test_powerio_server.py's ANDES-bootstrap helper into a
shared andes_mcp fixture in tests/conftest.py.
- Add tests/test_andes_server.py exercising run_power_flow,
get_system_info, run_time_domain_simulation, and the fixed
run_eigenvalue_analysis against the bundled Kundur case. Guarded by
pytest.importorskip("andes") like the existing ANDES bridge tests,
so they skip (not fail) in CI, which installs no extras. Verified
locally with a real andes 2.0.0 install: all pass.
- Document the new run_eigenvalue_analysis return shape and the
load_network_from_json/load_network_from_any tools (already in the
code, missing from the README) in ANDES/README.md; add a GPL-3.0
license note there and fill in powermcp/README.md's andes extras
row, which had an empty Notes column.
No CI workflow changes; PSS/E raw+dyr dynamic-model loading is
explicitly out of scope here (tracked separately).
Part of #1
run_power_flow(file_path) only loaded static topology -- no way to attach
a PSS/E .dyr dynamic-model file (generators, exciters, governors) to a
.raw case, so run_time_domain_simulation/run_eigenvalue_analysis had no
real dynamics to work with except on the one bundled all-JSON
ANDES/kundur_full.json fixture.
Adds an optional dyr_path: Optional[str] = None parameter, following the
function's own existing scaffolding exactly: resolved to an absolute
path and existence-checked before any run-dir/chdir work (same
error shape as the existing file_path check), copied into run_dir
alongside the main input via shutil.copy2 (same pattern), and passed as
addfile=<copied path> to andes.run(...) only when supplied -- the no-dyr
call path is byte-identical to before.
Adds two additive fields to the power_flow result:
- dynamic_models_loaded: dyr_path is not None
- n_dynamic_generators: ss.groups["SynGen"].n, read defensively via
getattr(..., 0) -- deliberately not get_system_info's num_generators
field, which sums PV.idx + GENROU.idx and is already non-zero from
static PV buses alone even with no .dyr loaded.
Tests (tests/test_andes_server.py) resolve ANDES's own bundled ieee14
PSS/E raw+dyr example case via andes.get_case(...) at test-call time
(inside each test body, after the andes_mcp fixture's
pytest.importorskip("andes") has already run) -- no new fixture file
is authored or vendored into this repo. andes.get_case() is a real,
documented top-level function (verified live against andes 2.0.0) that
resolves paths under the installed package's own andes/cases/
directory, declared as package-data in andes's own pyproject.toml.
Verified live (andes 2.0.0, real pip install):
- ieee14.raw alone: ss.groups['SynGen'].n == 0
- ieee14.raw + addfile=ieee14.dyr: ss.groups['SynGen'].n == 5
- All 9 tests in tests/test_andes_server.py pass (5 pre-existing + 4 new)
- Full suite in a clean venv with no extras: new tests skip cleanly,
106 passed / 13 skipped, no regressions
- Manually inspected the actual returned dict shape with and without
dyr_path, and for a missing dyr_path
Stacked on fix/andes-eigenvalue-analysis (PR #3): depends on its
run_eigenvalue_analysis fix and tests/conftest.py andes_mcp fixture.
Closes #2
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two combined fixes/additions to the ANDES MCP server (
ANDES/andes_mcp.py), motivated by wanting to use ANDES's small-signal/eigenvalue analysis for oscillation-risk review (per AEMO's 2026 General Power System Risk Review, which names small-signal stability and inter-area oscillation damping as active industry concerns, and separately notes system operators moving off proprietary small-signal tooling).1. Fix
run_eigenvalue_analysis(real bug, not a new feature)run_eigenvalue_analysisreadss.EIG.vectorsandss.EIG.state_descafter callingss.EIG.run(). Neither attribute exists on ANDES's realEIGroutine object — the code's ownhasattr()guards meanteigenvectors/state_variableshave silently returned[]since the tool was written, with no error and no test coverage catching it.Verified two ways: read
andes/routines/eig.pydirectly (GitHub + a local install), and installedandes2.0.0 into a venv and printedhasattr(ss.EIG, ...)for old vs. new attribute names directly. Real attributes:mu(eigenvalues, complex array),N/W(right/left eigenvector matrices),pfactors(participation factors),x_name(state labels).Fix: reads the real attributes and derives
frequency_hz/damping_ratio_pctper mode using the exact formula ANDES's ownEIG.post_process()uses internally for its text report:New return shape (documented in
ANDES/README.md):n_modes,modes(list of{eigenvalue: [re, im], frequency_hz, damping_ratio_pct, is_oscillatory}, sorted least-damped/most-concerning first),participation_factors,state_names,success.2. Add PSS/E raw+dyr dynamic-model loading to
run_power_flowrun_power_flowpreviously only loaded static topology — no way to attach a PSS/E.dyrdynamic-model file (generators, exciters, governors) to a.rawcase, sorun_time_domain_simulation/run_eigenvalue_analysishad no real dynamics to operate on except via the one bundled all-JSONANDES/kundur_full.jsonfixture.Added an optional
dyr_path: Optional[str] = Noneparameter, following the function's existing scaffolding exactly (abspath/exists-check, copy into the run directory, conditionaladdfilekwarg toandes.run(...)only when supplied — the no-dyr call path is byte-identical to before). Two new, purely additive return fields:dynamic_models_loadedandn_dynamic_generators(from ANDES'sSynGenmodel group, notget_system_info's existingnum_generators, which is already non-zero from staticPVbuses alone even with no.dyrloaded).No new fixture file is vendored into this repo. Tests resolve ANDES's own bundled
ieee14PSS/E example case at test-call time viaandes.get_case("ieee14/ieee14.raw")/andes.get_case("ieee14/ieee14.dyr")— a real, documented top-level function inandes2.0.0 (declared as package-data inandes's ownpyproject.toml), confirmed live:ss.groups["SynGen"].nis0loadingieee14.rawalone,5loading it withaddfile=ieee14.dyr. This avoids authoring a new.dyrfile by hand, which carries real risk given ANDES's own PSS/E dyr parser has known real-world compatibility gaps on some model types.Tests
tests/test_andes_server.py(new), using a sharedandes_mcpfixture intests/conftest.py(factored out oftests/test_powerio_server.py's prior ad hoc bootstrap): power flow convergence, system info, time-domain simulation, and the eigenvalue-analysis regression check (non-emptystate_names/numericfrequency_hz/damping_ratio_pctper mode — would have been empty before the fix) onkundur_full.json; plus four raw+dyr tests on ANDES's bundledieee14case (dynamics absent withoutdyr_path, present with it, enabling time-domain simulation, and a missing-file error path).All tests are
pytest.importorskip("andes")-guarded, matching this repo's existing convention for the optionalandesextra — they run and pass against a realandesinstall (verified locally: 9/9 passed) and skip cleanly (not fail) whenandesisn't installed (verified locally in a clean venv with no extras: 106 passed, 13 skipped, 0 failed).Docs
ANDES/README.md: documented both new/changed tool signatures and return shapes, added a license note (ANDES is GPL-3.0; installed only as an optional pip extra, never vendored into this MIT-licensed repo — including the raw+dyr test fixture, which is referenced from the installed package, not vendored), added a raw+dyr prompt example.powermcp/README.md: filled in the previously-empty Notes column for theandesextras-table row.